import * as path from "path";
import {
    Position,
    Range,
    TextDocumentEdit,
    TextEdit,
    WorkspaceEdit
} from "vscode-languageserver";
import { TextDocument } from "vscode-languageserver-textdocument";
import { t, util } from "./analysis";
import {
    metadataFlagToCompletionItemKind,
    symbolFlagToCompletionItemKind
} from "./translation";

export function isGeneratedFile(fsPath: string) {
    let lowerPath = fsPath.toLowerCase();
    return lowerPath.endsWith(t.GenerateFileExtens.metadata) || lowerPath.endsWith(t.GenerateFileExtens.routing);
}

export function getTopLevelSourceFile(source: t.SourceFile): t.SourceFile {
    let topSourceFile = source;
    while (true) {
        if (!topSourceFile.parent) {
            break;
        }
        topSourceFile = topSourceFile.parent.getSourceFile();
    }
    return topSourceFile;
}

const fullLabelParentSymbolFlags = t.SymbolKind.class
| t.SymbolKind.interface
| t.SymbolKind.enum
| t.SymbolKind.module
| t.SymbolKind.section
| t.SymbolKind.metadata;

function getSymbolFullName(symbol: t.Symbol): string {
    let items: string[] = [ symbol.name ];
    let parent = symbol.parent;
    while (parent && parent.is(fullLabelParentSymbolFlags)) {
        items.push(parent.name);
        parent = parent.parent;
    }
    return items.reverse().join(".");
}

function getSymbolModifiers(symbol: t.Symbol): string {
    let items: string[] = [];
    if (symbol.isCheckFlag(t.CheckFlag.readonly)) {
        items.push("ReadOnly");
    }
    if (symbol.isCheckFlag(t.CheckFlag.writeonly)) {
        items.push("WriteOnly");
    }
    if (symbol.isCheckFlag(t.CheckFlag.optional)) {
        items.push("Optional");
    }
    if (symbol.isCheckFlag(t.CheckFlag.paramArray)) {
        items.push("ParamArray");
    }
    return items.length > 0 ? items.join(" ") + " " : "";
}

export function getSymbolRanks(symbol: t.Symbol): string {
    let declaration = symbol.declaration;
    if (declaration.isDeclaration<t.ArrayDecl>(t.SyntaxKind.arrayDecl)) {
        let result = "";
        for (const d of declaration.dimensions) {
            result += `[${d.text}]`;
        }
        return result;
    }
    return symbol.type.rank !== undefined ? "[]".repeat(symbol.type.rank) : "";
}

export function getFinalSymbol(node: t.Node): t.Symbol | undefined {
    if (node.isIdentifier() ||
        node.isExpression<t.CallExpr>(t.SyntaxKind.callExpr) ||
        node.isExpression<t.IndexExpr>(t.SyntaxKind.indexExpr)
    ) {
        return node.symbol;
    }
    if (node.isExpression<t.MemberExpr>(t.SyntaxKind.memberExpr)) {
        return node.member.symbol;
    }
}

export function makeFullParameterDetailFromSymbol(symbol: t.Symbol): string {
    return symbol.parent && symbol.parent.is(t.SymbolKind.macroFunction) ?
        symbol.name :
        `${getSymbolModifiers(symbol)}${symbol.name} As ${symbol.type.name}${getSymbolRanks(symbol)}`;
}

function symbolFlagToString(symbol: t.Symbol): string {
    switch (symbol.kind & (~t.SymbolKind.undefined)) {
        case t.SymbolKind.variable:
        case t.SymbolKind.array:
            return "Dim";
        case t.SymbolKind.parameter:   return "(Parameter)";
        case t.SymbolKind.constant:    return "Const";
        case t.SymbolKind.function:
        case t.SymbolKind.method:
            if (symbol.type.is(t.TypeFlag.void)) {
                return "Sub";
            }
            return "Function";
        case t.SymbolKind.property:    return "Property";
        case t.SymbolKind.class:       return "Class";
        case t.SymbolKind.interface:   return "Interface";
        case t.SymbolKind.module:      return "Module";
        case t.SymbolKind.enum:        return "Enum";
        case t.SymbolKind.enumMember:  return "(Enum Member)";
        case t.SymbolKind.section:     return "(Section)";
        case t.SymbolKind.location:    return "(Label)";
        case t.SymbolKind.sourceFile:  return "(Source File)";
    }
    return "";
}

const metadataFlagToStringObject: { [key: number]: string } = {
    [t.SymbolMDMKind.areaName]: "areaname",
    [t.SymbolMDMKind.property]: "property",
    [t.SymbolMDMKind.template]: "template",
    [t.SymbolMDMKind.controlStyle]: "controlstyle",
    [t.SymbolMDMKind.cellStyle]: "cellstyle",
    [t.SymbolMDMKind.fontStyle]: "fontstyle",
    [t.SymbolMDMKind.style]: "style",
    [t.SymbolMDMKind.expression]: "expression",
    [t.SymbolMDMKind.elementType]: "elementtype",
    [t.SymbolMDMKind.factor]: "factor",
    [t.SymbolMDMKind.multiplier]: "multiplier",
    [t.SymbolMDMKind.other]: "other",
    [t.SymbolMDMKind.axis]: "axis",
    [t.SymbolMDMKind.precision]: "precision",
    [t.SymbolMDMKind.scale]: "scale",
    [t.SymbolMDMKind.codes]: "codes",
    [t.SymbolMDMKind.validation]: "validation",
    [t.SymbolMDMKind.initialAnswer]: "initialanswer",
    [t.SymbolMDMKind.defaultAnswer]: "defaultanswer",
    [t.SymbolMDMKind.helperFields]: "helperfields",
    [t.SymbolMDMKind.usageType]: "usagetype",
    [t.SymbolMDMKind.sublist]: "sublist",
    [t.SymbolMDMKind.element]: "element",
    [t.SymbolMDMKind.categories]: "categories",
    [t.SymbolMDMKind.field]: "field",
    [t.SymbolMDMKind.class]: "class",
};

function makeDetailPrefixFromSymbol(symbol: t.Symbol): string {
    if (!symbol.is(t.SymbolKind.metadata)) {
        return symbolFlagToString(symbol);
    }
    // metadata
    let text = metadataFlagToStringObject[symbol.mdmKind];
    return text ? `(${text})` : "";
}

function makeParametersDetailFromSymbol(symbol: t.Symbol): string {
    let items: string[] = [];
    if (symbol.is(t.SymbolKind.functionLike | t.SymbolKind.property)) {
        for (const param of symbol.parameters) {
            items.push(makeFullParameterDetailFromSymbol(param));
        }
    }
    return (items.length === 0 && !symbol.is(t.SymbolKind.functionLike | t.SymbolKind.property)) ? "" : symbol.is(t.SymbolKind.property) ? (items.length === 0 ? "" : `[${items.join(", ")}]`) : `(${items.join(", ")})`;
}

const typeSymbolFlag = t.SymbolKind.interface
| t.SymbolKind.class
| t.SymbolKind.module
| t.SymbolKind.enum
| t.SymbolKind.enumMember
| t.SymbolKind.metadata
| t.SymbolKind.macroFunction;

function makeDetailBindingTypeDetailFromSymbol(symbol: t.Symbol, node?: t.Node): string {
    if (symbol.is(typeSymbolFlag) ||
        (symbol.is(t.SymbolKind.function | t.SymbolKind.method) && symbol.type.is(t.TypeFlag.void)) // 无返回值的函数或方法，不必添加类型提示
    ) {
        return "";
    }
    return ` As ${((node ? symbol.tracker.getNodeType(node) : undefined) || symbol.type).name}${getSymbolRanks(symbol)}`;
}

export function makeDetailFromSymbol(symbol: t.Symbol, node?: t.Node): string {
    if (symbol.is(t.SymbolKind.undefined) && symbol.type.is(t.TypeFlag.any)) {
        return "any";
    }

    let result = makeDetailPrefixFromSymbol(symbol);
    if (result.length > 0) {
        result += " ";
    }
    // Macro
    if (symbol.is(t.SymbolKind.macro)) {
        let declaration = symbol.declaration;
        if (declaration.isPreprocessor<t.PPDefine>(t.SyntaxKind.ppDefine)) {
            result += declaration.text.replace(/(?<!\\)\\(?!\\)/g, "\\\\");
        }
        else {
            result += symbol.name;
        }
        return result;
    }

    result += getSymbolFullName(symbol);
    // (section) section_name or (section member) section_name.section_member_name
    if (symbol.is(t.SymbolKind.section | t.SymbolKind.sectionMember | t.SymbolKind.sourceFile | t.SymbolKind.location)) {
        return result;
    }
    // parameters
    result += `${makeParametersDetailFromSymbol(symbol)}${makeDetailBindingTypeDetailFromSymbol(symbol, node)}`;
    if (symbol.initialization !== undefined) {
        result += " = " + symbol.initialization.toString();
    }
    return result;
}

export function makeDocumentFromSymbol(symbol: t.Symbol, node?: t.Node, appendHeader = false): string {
    let result = appendHeader ? "```dmrs\n" + makeDetailFromSymbol(symbol, node) + "\n```\n" : "";
    let aheadComments = symbol.declaration.aheadComments;
    if (aheadComments && aheadComments.length > 0) {
        for (const c of aheadComments) {
            result += "\n" + c.comments.map(s => s.trim()).join("\n");
        }
    }
    return result;
}

export function getCompletionItemKindFromSymbol(symbol: t.Symbol) {
    return symbol.is(t.SymbolKind.metadata) ? metadataFlagToCompletionItemKind(symbol.mdmKind) : symbolFlagToCompletionItemKind(symbol.kind);
}

/**
 * 计算指定位置到指定节点范围的距离。
 * @param node 已知的节点
 * @param position 对比的字符位置
 * @returns 指定位置到节点的距离，如果在节点后，返回负数，如果在节点前，返回正数，在节点范围内，返回0
 */
export function distanceTo(node: t.ReadonlyTextRange, position: number): number {
    if (position >= node.start && position <= node.end) {
        return 0;
    }
    else if (position > node.end) {
        return node.end - position;
    }
    else {
        return node.start - position;
    }
}

export interface NodePath {
    stringLiteral?: t.StringLiteral;
    // dms sections
    inputDataSourceSection?: t.InputDataSourceSection;
    outputDataSourceSection?: t.OutputDataSourceSection;
    jobSection?: t.JobSection;
    globalSQLVariablesSection?: t.GlobalSQLVariablesSection;
    loggingSection?: t.LoggingSection;
    eventSection?: t.EventSection;
    metadataSection?: t.MetadataSection;
    outsideEventSection?: t.EventSection;
    routingSection?: t.RoutingSection;
    // script node
    categoricalLiteral?: t.CategoricalLiteral;
    indexExpression?: t.IndexExpr;
    memberExpression?: t.MemberExpr;
    callExpression?: t.CallExpr;
    localDeclarations?: t.LocalDecls;
    functionDeclaration?: t.FunctionDeclaration;
    withStatement?: t.WithStat;
    whileStatement?: t.WhileStat;
    doStatement?: t.DoStat;
    forStatement?: t.ForStat;
    forEachStatement?: t.ForEachStat;
    selectStatement?: t.SelectStat;
    selectClause?: t.SelectCaseClause;
    ifStatement?: t.IfStat;
    jumpStatement?: t.JumpStatement;
    // pre-processor
    preProIncludeNode?: t.PPInclude;
    preProIfNode?: t.PPIf;
    preProDefineNode?: t.PPDefine;
    preProErrorNode?: t.PPError;
    preProLineNode?: t.PPLine;
    // metadata
    metadataCategory?: t.MDMCategory;
}

export interface NodeAtPostionResult {
    node: t.Node | undefined;
    path: NodePath;
}

function updateNodePath(path: NodePath, node: t.Node) {
    switch (node.kind) {
        case t.SyntaxKind.stringLiteral:
            path.stringLiteral = node as t.StringLiteral;
            break;
        case t.SyntaxKind.categoricalLiteral:
            path.categoricalLiteral = node as t.CategoricalLiteral;
            break;
        // sections
        case t.SyntaxKind.inputDataSourceSection:
            path.inputDataSourceSection = node as t.InputDataSourceSection;
            break;
        case t.SyntaxKind.outputDataSourceSection:
            path.outputDataSourceSection = node as t.OutputDataSourceSection;
            break;
        case t.SyntaxKind.jobSection:
            path.jobSection = node as t.JobSection;
            break;
        case t.SyntaxKind.globalSQLVariablesSection:
            path.globalSQLVariablesSection = node as t.GlobalSQLVariablesSection;
            break;
        case t.SyntaxKind.loggingSection:
            path.loggingSection = node as t.LoggingSection;
            break;
        case t.SyntaxKind.eventSection:
            path.eventSection = node as t.EventSection;
            break;
        case t.SyntaxKind.metadataSection:
            path.metadataSection = node as t.MetadataSection;
            break;
        case t.SyntaxKind.routingSection:
            path.routingSection = node as t.RoutingSection;
            break;
        // declarations
        case t.SyntaxKind.localDecls:
            path.localDeclarations = node as t.LocalDecls;
            break;
        case t.SyntaxKind.funcDecl:
            path.functionDeclaration = node as t.FunctionDeclaration;
            break;
        // statements
        case t.SyntaxKind.ifStat:
            path.ifStatement = node as t.IfStat;
            break;
        case t.SyntaxKind.forStat:
            path.forStatement = node as t.ForStat;
            break;
        case t.SyntaxKind.forEachStat:
            path.forEachStatement = node as t.ForEachStat;
            break;
        case t.SyntaxKind.withStat:
            path.withStatement = node as t.WithStat;
            break;
        case t.SyntaxKind.whileStat:
            path.whileStatement = node as t.WhileStat;
            break;
        case t.SyntaxKind.doStat:
            path.doStatement = node as t.DoStat;
            break;
        case t.SyntaxKind.jumpStat:
            path.jumpStatement = node as t.JumpStatement;
            break;
        // preprocessor
        case t.SyntaxKind.ppDefine:
            path.preProDefineNode = node as t.PPDefine;
            break;
        case t.SyntaxKind.ppIf:
            path.preProIfNode = node as t.PPIf;
            break;
        case t.SyntaxKind.ppInclude:
            path.preProIncludeNode = node as t.PPInclude;
            break;
        // expression
        case t.SyntaxKind.memberExpr:
            path.memberExpression = node as t.MemberExpr;
            break;
        case t.SyntaxKind.indexExpr:
            path.indexExpression = node as t.IndexExpr;
            break;
        case t.SyntaxKind.callExpr:
            path.callExpression = node as t.CallExpr;
            break;
        // metadata
        case t.SyntaxKind.mdmCategory:
            path.metadataCategory = node as t.MDMCategory;
            break;
    }
}

export function getNodeAtPosition(node: t.Node, position: number, stopCallback: (n: t.Node) => boolean, nodeCallback?: (n: t.Node) => void): t.Node | undefined {
    return util.forEachChild(node,
        n => {
            if (distanceTo(n, position) !== 0) {
                return undefined;
            }
            if (nodeCallback) {
                nodeCallback(n);
            }
            // 取第一个符合条件的节点
            if (stopCallback(n)) {
                return n;
            }
            // 如果没有子节点，返回当前节点
            else if (n.childrenCount() === 0) {
                return n;
            }
            return getNodeAtPosition(n, position, stopCallback, nodeCallback);
        },
        na => {
            if (na.length === 0 || distanceTo(na, position) !== 0) {
                return undefined;
            }
            for (const item of na) {
                let distance = distanceTo(item, position);
                if (distance === 0) {
                    if (nodeCallback) {
                        nodeCallback(item);
                    }
                    let stop: t.Node | undefined;
                    if (stopCallback(item)) {
                        stop = item;
                    }
                    return getNodeAtPosition(item, position, stopCallback, nodeCallback) || stop;
                }
                if (distance > 0) {
                    return;
                }
            }
        });
}

export function inPreprocessor(nodePath: NodePath): boolean {
    return !!nodePath.preProDefineNode
        || !!nodePath.preProErrorNode
        || !!nodePath.preProIfNode
        || !!nodePath.preProIncludeNode
        || !!nodePath.preProLineNode;
}

export function inIterator(nodePath: NodePath, position: number): boolean {
    if (nodePath.forStatement && distanceTo(nodePath.forStatement.statement, position) !== 0) {
        return true;
    }
    if (nodePath.forEachStatement && distanceTo(nodePath.forEachStatement.statement, position) !== 0) {
        return true;
    }
    if (nodePath.whileStatement && distanceTo(nodePath.whileStatement.expression, position) === 0) {
        return true;
    }
    if (nodePath.doStatement && distanceTo(nodePath.doStatement.expression, position) === 0) {
        return true;
    }
    return false;
}

export function inCondition(nodePath: NodePath, position: number): boolean {
    if (nodePath.ifStatement && distanceTo(nodePath.ifStatement.condition, position) === 0) {
        return true;
    }
    if (nodePath.preProIfNode && distanceTo(nodePath.preProIfNode.condition, position) === 0) {
        return true;
    }
    if (nodePath.selectClause && distanceTo(nodePath.selectClause.conditions, position) === 0) {
        return true;
    }
    if (nodePath.selectStatement && distanceTo(nodePath.selectStatement.cases, position) !== 0) {
        return true;
    }
    if (nodePath.withStatement && distanceTo(nodePath.withStatement.statement, position) !== 0) {
        return true;
    }
    return false;
}

export function getNodeWithPathAtPosition(node: t.Node, position: number): NodeAtPostionResult {
    const path: NodePath = {};
    const nodeResult = getNodeAtPosition(node, position, n => n.childrenCount() === 0, n => updateNodePath(path, n));
    return { node: nodeResult, path };
}

function createTextEdit(start: Position, end: Position, newText: string): TextEdit {
    return {
        newText,
        range: Range.create(start, end)
    };
}

export function createWorkspaceEditorNotReplace(uri: string, start: Position, end: Position, text: string): WorkspaceEdit {
    let textEdit = createTextEdit(start, end, text);
    let edit = TextDocumentEdit.create({ version: null, uri }, [ textEdit ]);
    return {
        changes: { uri: [ textEdit ] },
        documentChanges: [ edit ]
    };
}

export function createWorkspaceEditorInsertContent(uri: string, start: Position, text: string) {
    return createWorkspaceEditorNotReplace(uri, start, start, text);
}

export function createRenameTextEdit(textDocument: TextDocument, node: t.Node, newText: string) {
    let start = textDocument.positionAt(node.start);
    let end = textDocument.positionAt(node.end);
    return createTextEdit(start, end, newText);
}

export function getProjectRootPath(fileName: string, workspaceFolderNames: string[]) {
    let lowerPath = fileName.toLowerCase();
    if (lowerPath.endsWith(t.GenerateFileExtens.metadata) ||
        lowerPath.endsWith(t.GenerateFileExtens.routing)
    ) {
        return lowerPath;
    }
    let validated = false;
    for (const name of workspaceFolderNames) {
        if (lowerPath.includes(name.toLowerCase())) {
            validated = true;
            break;
        }
    }
    if (!validated) {
        return lowerPath;
    }
    let fsPath = fileName;
    while (!workspaceFolderNames.some(e => path.basename(fsPath).toLowerCase() === e.toLowerCase())) {
        fsPath = path.dirname(fsPath);
        if (fsPath.endsWith("\\") || fsPath.endsWith("/") || fsPath === ".") {
            return path.dirname(fileName);
        }
    }
    return fsPath;
}

export function preprareMetadataFilePath(fsPath: string): { scriptFile: string, routingFile: string } {
    const folder = path.dirname(fsPath);
    const fileName = path.basename(fsPath, ".mdd");
    return {
        scriptFile: path.join(folder, fileName + t.GenerateFileExtens.metadata),
        routingFile: path.join(folder, fileName + t.GenerateFileExtens.routing)
    };
}
